package topoSort;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;

import javax.swing.JFrame;
import javax.swing.JButton;

import java.awt.Dimension;

import javax.swing.Box;

import java.awt.Component;

import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JSpinner;
import javax.swing.JTextArea;
import javax.swing.JScrollPane;
import javax.swing.SpinnerNumberModel;

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.Stack;
import java.util.Vector;

import javax.swing.border.LineBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class TopoSort extends JFrame implements ActionListener,
		KeyEventDispatcher
{
	private static final long serialVersionUID = 1L;
	private JTextArea taStack;
	private JTextArea taComment;
	private final Model model;
	private final Scene scene;
	private JButton btnReverse, btnAddVertex, btnDelete, btnAddEdge, btnFirst,
			btnNext, btnPrevious, btnLast, btnGenerate;
	private JSpinner spinVertexes;
	private JSpinner spinEdges;
	private JLabel lblStep;
	private JButton btnChangeNumber;

	/**
	 * Launch the application.
	 */
	public static void main(String[] args)
	{
		EventQueue.invokeLater(new Runnable()
		{
			public void run()
			{
				try
				{
					TopoSort frame = new TopoSort();
					frame.setVisible(true);
				}
				catch (Exception e)
				{
					e.printStackTrace();
				}
			}
		});
	}

	/**
	 * Create the frame.
	 */
	public TopoSort()
	{
		model = new Model();

		setTitle("Topological Sort Visualization");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setBounds(50, 30, 1280, 720);
		setIconImage(new ImageIcon(getClass().getResource("/images/icon.png"))
				.getImage());

		Box horizontalBox = Box.createHorizontalBox();
		getContentPane().add(horizontalBox, BorderLayout.CENTER);

		JScrollPane scrollPane = new JScrollPane();
		scrollPane.setViewportBorder(new LineBorder(new Color(0, 0, 0)));
		scrollPane.setAutoscrolls(true);
		horizontalBox.add(scrollPane);

		scene = new Scene(this);
		model.addObserver(scene);
		scene.setBounds(0, 0, 2000, 2000);
		scene.setBackground(Color.WHITE);
		scrollPane.setViewportView(scene);
		scene.setPreferredSize(new Dimension(1000, 1000));
		scene.setMaximumSize(new Dimension(1000, 1000));

		Box verticalBox = Box.createVerticalBox();
		verticalBox.setMaximumSize(new Dimension(300, 500));
		verticalBox.setAlignmentX(Component.CENTER_ALIGNMENT);
		horizontalBox.add(verticalBox);

		Box horizontalBox_2 = Box.createHorizontalBox();
		verticalBox.add(horizontalBox_2);

		Box verticalBox_3 = Box.createVerticalBox();
		horizontalBox_2.add(verticalBox_3);

		btnAddVertex = new JButton("(A)dd vertex");
		btnAddVertex.addActionListener(this);
		btnAddVertex.setToolTipText("Add new vertex to center of screen");
		verticalBox_3.add(btnAddVertex);

		btnDelete = new JButton("(Del)ete");
		btnDelete.addActionListener(this);
		btnDelete.setMaximumSize(new Dimension(400, 100));
		btnDelete.setToolTipText("Delete vertex or edge");
		verticalBox_3.add(btnDelete);

		btnPrevious = new JButton("Previous("+'\u2190'+")");
		btnPrevious.addActionListener(this);
		btnPrevious.setToolTipText("Previous step");
		btnPrevious.setMaximumSize(new Dimension(400, 100));
		verticalBox_3.add(btnPrevious);

		btnNext = new JButton("Next("+'\u2192'+")");
		btnNext.addActionListener(this);
		btnNext.setToolTipText("Next step");
		btnNext.setMaximumSize(new Dimension(400, 100));
		verticalBox_3.add(btnNext);

		Box verticalBox_4 = Box.createVerticalBox();
		horizontalBox_2.add(verticalBox_4);

		btnAddEdge = new JButton("(C)onnect");
		btnAddEdge.addActionListener(this);
		btnAddEdge.setMaximumSize(new Dimension(400, 100));
		btnAddEdge.setToolTipText("Connect two selected vertexes if possible");
		verticalBox_4.add(btnAddEdge);

		btnReverse = new JButton("(R)everse");
		btnReverse.addActionListener(this);
		btnReverse.setMaximumSize(new Dimension(400, 100));
		btnReverse.setToolTipText("Reverses the edge");
		verticalBox_4.add(btnReverse);

		btnFirst = new JButton("First("+'\u2191'+")");
		btnFirst.addActionListener(this);
		btnFirst.setMaximumSize(new Dimension(400, 100));
		btnFirst.setToolTipText("Go to initial graph");
		verticalBox_4.add(btnFirst);

		btnLast = new JButton("Last("+'\u2193'+")");
		btnLast.addActionListener(this);
		btnLast.setMaximumSize(new Dimension(400, 100));
		verticalBox_4.add(btnLast);

		lblStep = new JLabel("Step: 0");
		lblStep.setAlignmentX(Component.CENTER_ALIGNMENT);
		verticalBox.add(lblStep);

		JLabel lblStack = new JLabel("Stack");
		lblStack.setAlignmentX(Component.CENTER_ALIGNMENT);
		verticalBox.add(lblStack);

		taStack = new JTextArea();
		taStack.setLineWrap(true);
		taStack.setEditable(false);
		taStack.setToolTipText("State of stack on current move");
		taStack.setAlignmentY(Component.TOP_ALIGNMENT);
		verticalBox.add(taStack);
		taStack.setColumns(10);

		Component verticalGlue = Box.createVerticalGlue();
		verticalBox.add(verticalGlue);

		JLabel lblComment = new JLabel("Comment");
		lblComment.setAlignmentX(Component.CENTER_ALIGNMENT);
		verticalBox.add(lblComment);

		taComment = new JTextArea();
		taComment.setLineWrap(true);
		taComment.setEditable(false);
		taComment.setToolTipText("Explanation about current move");
		verticalBox.add(taComment);
		taComment.setColumns(10);

		btnGenerate = new JButton("(G)enerate");
		btnGenerate.addActionListener(this);

		btnChangeNumber = new JButton("Set number");
		btnChangeNumber.addActionListener(this);
		btnChangeNumber.setAlignmentX(Component.CENTER_ALIGNMENT);
		verticalBox.add(btnChangeNumber);
		btnGenerate.setToolTipText("Generate random graph");
		btnGenerate.setAlignmentX(Component.CENTER_ALIGNMENT);
		verticalBox.add(btnGenerate);

		Box horizontalBox_1 = Box.createHorizontalBox();
		verticalBox.add(horizontalBox_1);

		Box verticalBox_1 = Box.createVerticalBox();
		horizontalBox_1.add(verticalBox_1);

		JLabel lblVertices = new JLabel("Vertices");
		verticalBox_1.add(lblVertices);

		spinVertexes = new JSpinner();
		SpinnerNumberModel mVertices = (SpinnerNumberModel) (spinVertexes
				.getModel());
		mVertices.setMinimum(0);
		mVertices.setMaximum(100);
		spinVertexes.addChangeListener(new ChangeListener()
		{
			public void stateChanged(ChangeEvent e)
			{
				SpinnerNumberModel m = (SpinnerNumberModel) spinEdges
						.getModel();
				int n = (Integer) spinVertexes.getValue();
				int max = n * (n - 1) / 2;
				m.setMaximum(max);
				if ((Integer) m.getValue() > max)
				{
					m.setValue(max);
				}
			}
		});
		spinVertexes.setToolTipText("Vertices to add on random graph");
		verticalBox_1.add(spinVertexes);

		Box verticalBox_2 = Box.createVerticalBox();
		horizontalBox_1.add(verticalBox_2);

		JLabel lblEdges = new JLabel("Edges");
		verticalBox_2.add(lblEdges);

		spinEdges = new JSpinner();
		SpinnerNumberModel mEdges = (SpinnerNumberModel) (spinEdges.getModel());
		mEdges.setMinimum(0);
		spinEdges.setToolTipText("Count of edges on random graph");
		verticalBox_2.add(spinEdges);
		GraphGenerator.generate(model, scene, 5, 7);
		KeyboardFocusManager.getCurrentKeyboardFocusManager()
				.addKeyEventDispatcher(this);
		refreshUI();
	}

	private void reverseEdge()
	{
		Edge edge = scene.canReverse();
		if (edge == null)
		{
			return;
		}
		model.reverseLink(scene.getVertexId(edge.getFrom()),
				scene.getVertexId(edge.getTo()));
	}

	private void connectSelection()
	{
		try
		{
			Vector<Integer> a = new Vector<Integer>(scene.getSelectedIds());
			model.addLink(a.get(0), a.get(1));
		}
		catch (NullPointerException e)
		{
			e.printStackTrace();
		}
	}

	public void actionPerformed(ActionEvent e)
	{
		// TODO Auto-generated method stub
		Object src = e.getSource();
		if (src == btnAddVertex)
		{
			model.addVertex();
			model.sort();
			refreshUI();
		}
		else if (src == btnAddEdge)
		{
			connectSelection();
			model.sort();
			refreshUI();
		}
		else if (src == btnDelete)
		{
			removeSelection();
			model.sort();
			scene.repaint();
			refreshUI();
		}
		else if (src == btnReverse)
		{
			reverseEdge();
			model.sort();
			refreshUI();
		}
		else if (src == btnFirst)
		{
			model.setFirstState();
			refreshUI();
		}
		else if (src == btnNext)
		{
			model.setNextState();
			refreshUI();
		}
		else if (src == btnPrevious)
		{
			model.setPrevState();
			refreshUI();
		}
		else if (src == btnGenerate)
		{
			GraphGenerator.generate(model, scene,
					(Integer) spinVertexes.getValue(),
					(Integer) spinEdges.getValue());
			refreshUI();
		}
		else if (src == btnLast)
		{
			model.setLastState();
			refreshUI();
		}
		else if (src == btnChangeNumber)
		{
			Integer selectedId = (Integer) scene.getSelectedIds().toArray()[0];
			Object possibleIds[] = model.getIncidentList().keySet().toArray();
			Object o = JOptionPane.showInputDialog(this, null, "Swap number of vertex " + selectedId,
					JOptionPane.QUESTION_MESSAGE, null, possibleIds, selectedId);
			if (o == null)
			{
				return;
			}
			Integer toSwap = (Integer) o;
			if (selectedId == toSwap)
			{
				return;
			}
			model.swapVertexNumber(selectedId, toSwap);
			refreshUI();
		}
	}

	public void refreshUI()
	{
		int cur = model.getCurrentMove();
		int max = model.getNumOfMoves();
		boolean bad = model.hasBadLink();
		btnNext.setEnabled(cur != max && !bad);
		btnPrevious.setEnabled(cur != 0 && !bad);
		btnLast.setEnabled(cur != max && !bad);
		btnFirst.setEnabled(cur != 0 && !bad);
		btnDelete.setEnabled(scene.getSelectionCount() > 0 && cur == 0);
		btnAddEdge.setEnabled(connectable() && cur == 0);
		btnAddVertex.setEnabled(cur == 0);
		btnReverse.setEnabled(scene.canReverse() != null && cur == 0);
		btnChangeNumber.setEnabled(scene.getSelectedIds().size() == 1 && cur == 0);
		if (bad)
		{
			taComment
					.setText("There is a cycle in graph, \nred edge takes part in it \nand algorithm can't do \nanything here.");
		}
		else if (cur < max - model.getIncidentList().size())
		{
			taComment
					.setText("We are performing depth-first\nsearch (DFS) to find the order of\nnodes we need.\nColors:\ngray - in DFS's stack\nblack - have been visited\nblue - were not visited.");
		}
		else if (cur != max)
		{
			taComment
					.setText("We now extract elements from stack and number vertices in the order of extraction.");
		}
		else
		{
			taComment
					.setText("Algoright has ended working.\nResult is the numbers of nodes.");
		}

		Stack<Integer> stack = model.getCurrentStack();
		String stackStr = new String();
		for (Integer id : stack)
		{
			stackStr += id + " ";
		}
		taStack.setText(stackStr);

		if (bad)
		{
			lblStep.setText("Cycle in graph.");
		}
		else
		{
			lblStep.setText("Step: " + cur + " of " + max);
		}
	}

	private boolean connectable()
	{
		return (scene.getSelectedIds().size() == 2);
	}

	private void removeSelection()
	{
		// Remove edges first
		for (Edge e : scene.getSelectedEdges())
		{
			model.removeLink(scene.getVertexId(e.getFrom()),
					scene.getVertexId(e.getTo()));
		}
		// Then remove vertices
		for (Integer id : scene.getSelectedIds())
		{
			model.removeVertex(id);
		}
	}

	public boolean dispatchKeyEvent(KeyEvent e)
	{
		if (e.getID() == KeyEvent.KEY_PRESSED)
		{
			switch (e.getKeyCode())
			{
			case KeyEvent.VK_C:
				btnAddEdge.doClick();
				break;
			case KeyEvent.VK_A:
				btnAddVertex.doClick();
				break;
			case KeyEvent.VK_R:
				btnReverse.doClick();
				break;
			case KeyEvent.VK_DELETE:
				btnDelete.doClick();
				break;
			case KeyEvent.VK_LEFT:
				btnPrevious.doClick();
				break;
			case KeyEvent.VK_RIGHT:
				btnNext.doClick();
				break;
			case KeyEvent.VK_UP:
				btnFirst.doClick();
				break;
			case KeyEvent.VK_DOWN:
				btnLast.doClick();
				break;
			case KeyEvent.VK_G:
				btnGenerate.doClick();
				break;
			case KeyEvent.VK_P:
				if (e.isAltDown())
				{
					scene.setPicachu();
				}
			}
			return true;
		}
		return false;
	}

}
